Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.39% covered (success)
94.39%
185 / 196
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
94.39% covered (success)
94.39%
185 / 196
50.00% covered (danger)
50.00%
2 / 4
55.53
0.00% covered (danger)
0.00%
0 / 1
 postRun
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyId
86.21% covered (warning)
86.21%
50 / 58
0.00% covered (danger)
0.00%
0 / 1
19.95
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 patch
97.69% covered (success)
97.69%
127 / 130
0.00% covered (danger)
0.00%
0 / 1
29
1<?php
2namespace Apie\DoctrineEntityConverter\CodeGenerators;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Entities\RequiresRecalculatingInterface;
6use Apie\Core\Identifiers\AutoIncrementInteger;
7use Apie\Core\Metadata\MetadataFactory;
8use Apie\Core\Utils\ConverterUtils;
9use Apie\DoctrineEntityConverter\Concerns\HasGeneralDoctrineFields;
10use Apie\DoctrineEntityConverter\Concerns\RequiresDomainUpdate;
11use Apie\DoctrineEntityConverter\Entities\SearchIndex;
12use Apie\StorageMetadata\Attributes\AclLinkAttribute;
13use Apie\StorageMetadata\Attributes\DiscriminatorMappingAttribute;
14use Apie\StorageMetadata\Attributes\GetMethodAttribute;
15use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
16use Apie\StorageMetadata\Attributes\ManyToOneAttribute;
17use Apie\StorageMetadata\Attributes\OneToManyAttribute;
18use Apie\StorageMetadata\Attributes\OneToOneAttribute;
19use Apie\StorageMetadata\Attributes\OrderAttribute;
20use Apie\StorageMetadata\Attributes\ParentAttribute;
21use Apie\StorageMetadata\Attributes\PropertyAttribute;
22use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface;
23use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
24use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
25use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
26use Apie\TypeConverter\ReflectionTypeFactory;
27use Doctrine\Common\Collections\Collection;
28use Doctrine\ORM\Mapping\Column;
29use Doctrine\ORM\Mapping\Entity;
30use Doctrine\ORM\Mapping\GeneratedValue;
31use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
32use Doctrine\ORM\Mapping\Id;
33use Doctrine\ORM\Mapping\JoinColumn;
34use Doctrine\ORM\Mapping\ManyToMany;
35use Doctrine\ORM\Mapping\ManyToOne;
36use Doctrine\ORM\Mapping\OneToMany;
37use Doctrine\ORM\Mapping\OneToOne;
38use Doctrine\ORM\Mapping\OrderBy;
39use Generator;
40use Nette\PhpGenerator\Attribute;
41use Nette\PhpGenerator\ClassType;
42use Nette\PhpGenerator\PromotedParameter;
43use Nette\PhpGenerator\Property;
44use ReflectionClass;
45use ReflectionProperty;
46
47/**
48 * Adds created_at and updated_at and Doctrine attributes
49 */
50class AddDoctrineFields implements PostRunGeneratedCodeContextInterface
51{
52    public function postRun(GeneratedCodeContext $generatedCodeContext): void
53    {
54        foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) {
55            $this->patch($generatedCodeContext, $code);
56        }
57    }
58
59    private function applyId(ClassType $classType): void
60    {
61        $property = null;
62        $doctrineType = null;
63        $nullable = false;
64        $generatedValue = false;
65        if ($classType->hasProperty('id')) {
66            $property = $classType->getProperty('id');
67        } elseif ($classType->hasProperty('search_id')) {
68            $property = $classType->getProperty('search_id')->cloneWithName('id');
69            $classType->addMember($property);
70        }
71        if ($property === null) {
72            $property = $classType->addProperty('id')->setType('?int');
73            $generatedValue = true;
74            $doctrineType = 'integer';
75        } else {
76            // @see ClassTypeFactory
77            $originalClass = $classType->getComment();
78            if ($originalClass && class_exists($originalClass)) {
79                $metadata = MetadataFactory::getResultMetadata(
80                    new ReflectionClass($originalClass),
81                    new ApieContext()
82                );
83                $hashmap = $metadata->getHashmap();
84                if (isset($hashmap['id'])) {
85                    $type = $hashmap['id']->getTypehint();
86                    $nullable = $hashmap['id']->allowsNull();
87                    $class = ConverterUtils::toReflectionClass($type);
88                    if ($class && $class->isSubclassOf(AutoIncrementInteger::class)) {
89                        $generatedValue = true;
90                        $nullable = false;
91                        $property->setInitialized(true);
92                    }
93                    $scalarType = MetadataFactory::getScalarForType($hashmap['id']->getTypehint(), true);
94                    $property->setType(
95                        $scalarType->value
96                    );
97                    $doctrineType = $scalarType->toDoctrineType();
98                }
99            }
100        }
101
102        if (in_array(AutoIncrementTableInterface::class, $classType->getImplements())
103            || in_array(MixedStorageInterface::class, $classType->getImplements())) {
104            $generatedValue = true;
105            $nullable = false;
106        }
107
108        $hasIdAttribute = false;
109        $hasColumnAttribute = false;
110        foreach ($property->getAttributes() as $attribute) {
111            if (in_array($attribute->getName(), [Column::class, ManyToOne::class, OneToMany::class, ManyToMany::class])) {
112                $hasColumnAttribute = true;
113                break;
114            }
115            if ($attribute->getName() === GeneratedValue::class) {
116                $generatedValue = false;
117            }
118            if ($attribute->getName() === Id::class) {
119                $hasIdAttribute = true;
120            }
121        }
122        if (!$hasIdAttribute) {
123            $property->addAttribute(Id::class);
124        }
125        if (!$hasColumnAttribute) {
126            if ($doctrineType === null) {
127                $doctrineType = MetadataFactory::getScalarForType(
128                    ReflectionTypeFactory::createReflectionType($property->getType()),
129                    true
130                )->toDoctrineType();
131            }
132            $property->addAttribute(Column::class, ['type' => $doctrineType, 'nullable' => $nullable]);
133        }
134        if ($generatedValue) {
135            $property->addAttribute(GeneratedValue::class);
136        }
137    }
138
139    /**
140     * @return Generator<int, PromotedParameter|Property>
141     */
142    private function iterateProperties(ClassType $classType): Generator
143    {
144        foreach ($classType->getProperties() as $property) {
145            yield $property;
146        }
147        if ($classType->hasMethod('__construct')) {
148            foreach ($classType->getMethod('__construct')->getParameters() as $parameter) {
149                if ($parameter instanceof PromotedParameter) {
150                    yield $parameter;
151                }
152            }
153        }
154    }
155
156    private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void
157    {
158        $classType->addAttribute(Entity::class);
159        $classType->addAttribute(HasLifecycleCallbacks::class);
160        $classType->addTrait('\\' . HasGeneralDoctrineFields::class);
161
162        // @see ClassTypeFactory
163        $originalClass = $classType->getComment();
164        if ($originalClass && class_exists($originalClass)) {
165            if (is_a($originalClass, RequiresRecalculatingInterface::class, true)) {
166                $classType->addTrait('\\' . RequiresDomainUpdate::class);
167            }
168        }
169
170        foreach ($this->iterateProperties($classType) as $property) {
171            $added = false;
172            foreach ($property->getAttributes() as $attribute) {
173                switch ($attribute->getName()) {
174                    case GetMethodAttribute::class:
175                    case PropertyAttribute::class:
176                        $added = true;
177                        if (in_array($property->getType(), ['DateTimeImmutable', '?DateTimeImmutable'])) {
178                            $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']);
179                        } else {
180                            $arguments = $attribute->getArguments();
181                            if ($arguments[2] ?? false) {
182                                $property->addAttribute(Column::class, ['nullable' => true, 'type' => 'text']);
183                            } else {
184                                $property->addAttribute(Column::class, ['nullable' => true]);
185                            }
186                        }
187                        break;
188                    case DiscriminatorMappingAttribute::class:
189                        $added = true;
190                        $property->addAttribute(Column::class, ['type' => 'json']);
191                        break;
192                    case ManyToOneAttribute::class:
193                        $added = true;
194                        $targetEntity = $property->getType();
195                        $property->addAttribute(
196                            ManyToOne::class,
197                            [
198                                'targetEntity' => $targetEntity,
199                                'inversedBy' => $attribute->getArguments()[0],
200                            ]
201                        );
202                        $property->addAttribute(
203                            JoinColumn::class,
204                            [
205                                'nullable' => true,
206                            ]
207                        );
208                        break;
209                    case OneToManyAttribute::class:
210                    case AclLinkAttribute::class:
211                        $added = true;
212                        $property->setType(Collection::class);
213                        if ($attribute->getName() === OneToManyAttribute::class) {
214                            $targetEntity = $attribute->getArguments()[1];
215                            $mappedByProperty = $generatedCodeContext->findParentProperty($targetEntity);
216                            $mappedByProperty ??= $attribute->getArguments()[0];
217                            $mappedByProperty ??= 'ref_' . $classType->getName();
218                        } else {
219                            $targetEntity = $attribute->getArguments()[0];
220                            $mappedByProperty = 'ref_' . $classType->getName();
221                        }
222                        $indexByProperty = $generatedCodeContext->findIndexProperty($targetEntity);
223                        if ($indexByProperty) {
224                            $property->addAttribute(OrderBy::class, [[$indexByProperty => 'ASC']]);
225                        }
226                        $property->addAttribute(
227                            OneToMany::class,
228                            [
229                                'cascade' => ['all'],
230                                'targetEntity' => $targetEntity,
231                                'mappedBy' => $mappedByProperty,
232                                'fetch' => 'EAGER',
233                                'indexBy' => $indexByProperty,
234                                'orphanRemoval' => true,
235                            ]
236                        );
237                        break;
238                    case OneToOneAttribute::class:
239                        $added = true;
240                        $targetEntity = $property->getType();
241                        // look for @ParentAttribute for inversedBy?
242                        $property->addAttribute(
243                            OneToOne::class,
244                            [
245                                'cascade' => ['all'],
246                                'targetEntity' => $targetEntity,
247                                'fetch' => 'EAGER',
248                            ]
249                        );
250                        break;
251                    case GetSearchIndexAttribute::class:
252                        $added = true;
253                        $property->setType(Collection::class);
254                        $searchTableName = strpos($classType->getName(), 'apie_resource__') === 0
255                            ? preg_replace('/^apie_resource__/', 'apie_index__', $classType->getName())
256                            : 'apie_index__' . $classType->getName();
257                        $searchTableName .= '_' . $property->getName();
258                        $searchTable = SearchIndex::createFor(
259                            $searchTableName,
260                            $classType->getName(),
261                            $property->getName(),
262                        );
263                        $generatedCodeContext->generatedCode->generatedCodeHashmap[$searchTableName] = $searchTable;
264                        $property->addAttribute(
265                            OneToMany::class,
266                            [
267                                'cascade' => ['all'],
268                                'targetEntity' => $searchTableName,
269                                'mappedBy' => 'parent',
270                                'orphanRemoval' => true,
271                            ]
272                        );
273                        $args = $attribute->getArguments();
274                        $args['arrayValueType'] = $searchTableName;
275                        // there is no good method in nette/php-generator
276                        (new ReflectionProperty(Attribute::class, 'args'))->setValue($attribute, $args);
277                        $type = $property->getType();
278                        break;
279                    case OrderAttribute::class:
280                        $added = true;
281                        $type = 'text';
282                        if ($property->getType() === 'int') {
283                            $type = 'integer';
284                        }
285                        $property->addAttribute(Column::class, ['type' => $type]);
286                        break;
287                    case ParentAttribute::class:
288                        $added = true;
289                        $inversedBy = $generatedCodeContext->findInverseProperty($property->getType(), $classType->getName());
290                        $property->addAttribute(
291                            ManyToOne::class,
292                            ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy]
293                        );
294                        break;
295                }
296            }
297            if (!$added) {
298                $type = $property->getType();
299                switch ((string) $type) {
300                    case 'string':
301                        $property->addAttribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]);
302                        break;
303                    case 'float':
304                        $property->addAttribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]);
305                        break;
306                    case 'int':
307                    case '?int':
308                        $property->addAttribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]);
309                        break;
310                    case 'array':
311                    case '?array':
312                        $property->addAttribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]);
313                        break;
314                }
315            }
316        }
317
318        $this->applyId($classType);
319    }
320}